resource.AddFile("sound/StargateExtras/InBeamCharge.wav")
resource.AddFile("sound/StargateExtras/InBeamLoop.wav")
resource.AddFile("models/pyro_overloader/overloader.mdl")

AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")

include("shared.lua")
include("autorun/server/StargateExtras.lua")

-- Sound when firing begins
local fireSoundPath = Sound("StargateExtras/InBeamCharge.wav")
-- Ambient sound while firing
local beamSoundPath = Sound("StargateExtras/InBeamLoop.wav")
-- Sound when a valid target gate is detected and the overloader prepares to fire
local startupSoundPath  = Sound("NPC_FloorTurret.Deploy")
-- Sound when the overloader shuts down (no longer able to fire)
local shutdownSoundPath = Sound("NPC_FloorTurret.Retire")

local defaultModel = "models/pyro_overloader/overloader.mdl"

-- Animation names
local startupAnimName = "open"
local shutdownAnimName = "idle"
local firingAnimName = "firing"

-- A multiplier for the amount of energy needed to destroy a stargate.
-- i.e. energyNeeded = gate.capacity * energyMultiplier
local energyMultiplier = 5

local MAX_GATE_DISTANCE = 2000
local MIN_ENERGY_USAGE = 1000
local MAX_ENERGY_USAGE = 10000

-- The time when the USE key was last pressed on this entity
local lastUseTime = 0

-- The numbe of seconds between each Think() call
ENT.cycleInterval = 0.2

-- The beam that is being fired
ENT.beam = nil
-- The gate on the overloader's end of the wormhole
ENT.localGate = nil
-- The gate on the other end of the wormhole
ENT.remoteGate = nil

-- Whether the weapon should prepare to fire
ENT.isArmed = false
-- Whether the weapon has a gate targetted and is ready to fire
ENT.isActive = false
-- Whether the weapon is firing into a gate
ENT.isFiring = false
-- Whether the weapon is exploding
ENT.isDestructing = false
-- Whether the beam should come out of the other end of the wormhole (as with the Asuran satellite weapon)
ENT.isBeamCoherent = false

-- The stargate overloader is immune to EMP, as seen in the series
ENT.CDSEmp_Ignore = true

-- Spawns a gate overloader for the given player 
function ENT:SpawnFunction(player, trace)
	if(!trace.Hit) then 
      return 
   end
   
	local cannon = ents.Create(self:GetClass())
	cannon:SetPos(trace.HitPos + trace.HitNormal * cannon:BoundingRadius())
   cannon:SetVar("Owner", player)
	cannon:Spawn()
	cannon:Activate()
   
	undo.Create(self:GetClass())
	undo.AddEntity(cannon)
	undo.SetPlayer(player)
	undo.Finish()
end

-- Sets the gate overloader's model, physics, health, resources, wire inputs/outputs, etc.
function ENT:Initialize()
	if(util.IsValidModel(self.Entity:GetModel()) == false ||
      self.Entity:GetModel() == "models/error.mdl") then
      Msg("Using default model for "..self:GetClass()..".\n")
      self.Entity:SetModel(defaultModel)
      
      if(util.IsValidModel(self.Entity:GetModel()) == false) then
         error("No valid model for "..self:GetClass()..".\n")
      end
   end
   
   self.Entity:SetSolid(SOLID_VPHYSICS)
	self.Entity:PhysicsInit(SOLID_VPHYSICS)
	
   local physics = self.Entity:GetPhysicsObject()
   
	if(physics && physics:IsValid()) then 
      physics:Wake()
   end
   
   -- Round up energy requirement
   self:SetEnergyUsage(math.ceil(self.energyPerSecond))
   
   -- Set health
   self.Entity:SetMaxHealth(300)
   self.Entity:SetHealth(300)
	
   -- Reset states (this sets them as networked variables)
   self:SetIsBeamCoherent(self:IsBeamCoherent())
   self:SetIsActive(false)
   self:SetIsFiring(false)
   
   -- Set resources
   if(self.HasRD) then
      self:AddResource("energy", 1)
   end
    
    -- Set up wire inputs and outputs
	if(self.HasWire) then
		self:CreateWireInputs("Fire", "Focus Beam") 
		self:CreateWireOutputs("Energy", "Time") 
	end
   
   StargateExtras:RegisterEntity(self)
end

-- Respond to a given wire input
function ENT:TriggerInput(inputName, inputValue)
   if(inputName == "Fire") then
      if(inputValue ~= 0) then
         self:Arm()
      else
         self:Disarm()
      end
   elseif(inputName == "Focus Beam") then
      self:SetIsBeamCoherent(inputValue ~= 0)
   end
end

-- Toggle armed state when the USE key is pressed on the overloader
function ENT:Use()
   if(lastUseTime + 1 >= CurTime()) then
      return
   end
   
   if(self.isArmed == false) then
      self:Arm()
   else
      self:Disarm()
   end
   
   lastUseTime = CurTime()
end

-- Sets the amount of energy the overloader should use per second
function ENT:SetEnergyUsage(energyUsage)
   if(energyUsage < MIN_ENERGY_USAGE) then
      energyUsage = MIN_ENERGY_USAGE
   elseif(energyUsage > MAX_ENERGY_USAGE) then
      energyUsage = MAX_ENERGY_USAGE
   end
   
   self.energyPerSecond = energyUsage
   self.energyPerCycle = energyUsage * self.cycleInterval
end

-- Returns a valid target stargate (nearby and in front of the overloader)
function ENT:FindTarget()
	for _, gate in pairs(ents.FindByClass("stargate_*")) do
		local gateDistance = self.Entity:GetPos():Distance(gate:GetPos())
      
		if(gateDistance < MAX_GATE_DISTANCE && self:IsAimedAtGate(gate)) then
         return gate
		end
	end

   return nil
end

-- Returns whether there is Line-Of-Sight between the overloader's emitter and the given entity
function ENT:IsAimedAtGate(gate)
   if(gate == nil || gate:IsValid() == false) then 
      return false
   end

   local gateCentre = StargateExtras:GetEntityCentre(gate)
   local emitterPos = self:GetEmitterPos()
   
   local gateDirection = gateCentre - emitterPos
   local emitterDirection = emitterPos - gateCentre
   
   local angleToGate = gateDirection:GetNormal():Dot(self.Entity:GetAngles():Forward())
   local angleFromGate = emitterDirection:GetNormal():Dot(gate:GetAngles():Forward())
   
   -- If the cannon is not facing almost directly at the gate, return false
   if(angleToGate < 0.98 || angleFromGate < 0.98) then
      return false
   end
   
   local vector = (gateCentre - emitterPos) * 1.1
   local ignorableEntities = { self.Entity }
   self.trace = StarGate.Trace:New(emitterPos, 
                                   vector,
                                   ignorableEntities)
   
   local traceEnt = self.trace.Entity
   
   -- If we had LOS on a gate, but now are hitting a player/NPC, return true so the overloader doesn't shut off
   -- (the player/NPC will be disintegrated shortly and LOS should be restored)
   if(ValidEntity(self.localGate) && 
      ValidEntity(traceEnt) && (traceEnt:IsPlayer() || traceEnt:IsNPC())) then
      
      return true
   end
   
   local hasTraceHitGate = ValidEntity(traceEnt) && 
                           (traceEnt == gate || 
                            traceEnt == gate.EventHorizon)
   
   return hasTraceHitGate
end

-- Returns whether the overloader is upright
function ENT:IsUpright()
   return self.Entity:GetAngles():Up():Dot(Vector(0,0,1)) > 0.8
end

-- Returns whether the given gate is a valid target
function ENT:IsGateValidTarget(gate)
   if(gate ~= nil && gate:IsValid() && self:IsAimedAtGate(gate)) then
      return true
   else
      return false
   end
end

function ENT:SetLocalGate(gate)
   self.localGate = gate
   self.Entity:SetNetworkedEntity("localGate", gate)
end

-- Clears the current target and shuts down the overloader
function ENT:ClearTarget()
   self:Shutdown()
   
   self:SetLocalGate(nil)
end

-- Handles state transitions and cools down all stargates
function ENT:Think()
   if(self.localGate && self:IsGateValidTarget(self.localGate) == false) then
      self:ClearTarget()
   end
   
   if(self.isArmed) then
      if(self:IsUpright()) then
      	if(self.isActive) then
      		self:FireBeam()
      	else
            self.localGate = self:FindTarget()
            
            if(self.localGate) then
               self:Startup()
            end
         end
      else
         self:Shutdown()
      end
   end
   
	self.Entity:NextThink(CurTime() + self.cycleInterval)
	return true
end

-- Updates all wire output values
function ENT:UpdateWireOutputs()
   if(!self.HasWire) then
      return
   end
   
   if(self.isFiring) then
      local energyRequired = self.remoteGate.excessPowerLimit - 
                             self.remoteGate.excessPower
      local timeLeft = (energyRequired / self.energyPerSecond)
      
      self:SetWire("Energy", energyRequired)
      self:SetWire("Time", timeLeft)
   else
      self:SetWire("Time", -1)
   end
end

-- Prepares the overloader to fire
-- Returns: Whether startup was successful
function ENT:Startup()
   if(self.isArmed == false) then
      return false
   elseif(self.isActive == true) then
      return true
   end
   
   local readyAnimation = self.Entity:LookupSequence(startupAnimName)
   
   if(readyAnimation ~= -1) then
      self.Entity:ResetSequence(readyAnimation)
   end
   
   self.Entity:EmitSound(startupSoundPath, 80, 100)
   
   self:SetLocalGate(self.localGate) -- Sets the local gate as a networked entity
   
   if(self.HasRD) then
      -- Allow the overloader to store the energy it needs for one second of fire
      self:AddResource("energy", self.energyPerSecond)
   end
   
   self:SetIsActive(true)
   
   return true
end

-- Returns: whether the overloader could be shutdown
function ENT:Shutdown()
   if(self.isActive == false) then
      return true
   end
   
   self:StopFiring()
   
   if(self.HasRD) then
      -- Remove energy storage while the device is not active
      self:AddResource("energy", 1)
   end
   
   self.Entity:EmitSound(shutdownSoundPath, 100, 100)
   
   local idleAnimation = self.Entity:LookupSequence(shutdownAnimName)
   
   if(idleAnimation ~= -1) then
      self.Entity:SetSequence(idleAnimation)
   end
   
   self:SetIsActive(false)
   
   self:UpdateWireOutputs()
     
   return true
end

-- Allows the overloader to acquire a target gate and fire when ready
-- Returns: whether the overloader could be armed
function ENT:Arm()
   self.isArmed = true
   
   return true
end

-- Shuts down the overloader and prevents it from firing or acquiring a new target
-- Returns: whether the overloader could be disarmed
function ENT:Disarm()
   if(self.isArmed == false) then
      return true
   end
   
   self:Shutdown()
   self.isArmed = false
   
   self:UpdateWireOutputs()
   
   return true
end

-- Fires the beam
-- Returns: whether firing succeeded
function ENT:FireBeam()
   if(self.isArmed == false || self.isActive == false || 
      self:IsGateValidTarget(self.localGate) == false ||
      StargateExtras:IsStargateOpen(self.localGate) == false || 
      StargateExtras:GetRemoteStargate(self.localGate) == nil) then
      self:StopFiring()
      return false
   elseif(StargateExtras:IsIrisClosed(self.localGate)) then
      self.Entity:SetOverlayText("Unable To Fire\nIris Closed")
      self:StopFiring()
      return false
   elseif(StargateExtras:IsStargateOutbound(self.localGate) == false) then
      self.Entity:SetOverlayText("Unable To Fire\nWormhole Incoming")
      self:StopFiring()
      return false
   end
   
   if(self.HasRD) then
      local energyAvailable = self:GetResource("energy")
      
      -- If there isn't enough energy left to power the beam for another second, stop firing
      if(energyAvailable < self.energyPerSecond) then
         self.Entity:SetOverlayText(self.energyPerSecond .. " Energy Required")
         self:StopFiring()
         return false
      end
   end
   
   self.remoteGate = StargateExtras:GetRemoteStargate(self.localGate)
   
   if(self.remoteGate == nil || self.remoteGate:IsValid() == false) then
      self:StopFiring()
      return false
   end
   
   -- If this is the starting shot
   if(self.isFiring == false) then
      self:StartFiring()
   end
   
   if(self.beamSound) then
      self.beamSound:Play()
   end
   
   -- Make sure any DHDs near the gate are jammed
   -- Do this constantly while firing to prevent players spawning a DHD in order to shut down the gate
   for _, dhd in pairs(self.remoteGate:FindDHD()) do
      StargateExtras:JamDHD(dhd, self.cycleInterval * 2)
   end
   
   self:SetIsFiring(true)
   self.Entity:SetOverlayText("")
   
   if(self.beam == nil) then
      self.beam = self:CreateBeam()
   end
   
   self:HeatGate(self.remoteGate)
   self:UpdateWireOutputs()
   
   return true
end

function ENT:StartFiring()
   self.Entity:EmitSound(fireSoundPath, 100, 100)
   
   local firingAnimation = self.Entity:LookupSequence(firingAnimName)
   
   if(firingAnimation ~= -1) then
      self.Entity:SetSequence(firingAnimation)
   end
   
   StargateExtras:JamRemoteGate(self.remoteGate)
   
   if(StargateExtras:GetStargateEnergyCapacity(self.remoteGate) == nil) then
      StargateExtras:SetStargateEnergyCapacity(self.remoteGate, StargateExtras.STARGATE_DEFAULT_ENERGY_CAPACITY)
   end
   
   if(self.remoteGate.excessPowerLimit == nil) then
      self.remoteGate.excessPowerLimit = StargateExtras:GetStargateEnergyCapacity(self.remoteGate) * 
                                         energyMultiplier
   end
   
   self.remoteGate.overloader = self.Entity
   self.Entity:SetNetworkedEntity("remoteGate", self.remoteGate)
end

function ENT:SetIsActive(isActive)
   self.isActive = util.tobool(isActive)
   self.Entity:SetNetworkedBool("isActive", self.isActive)
end

function ENT:SetIsFiring(isFiring)
   self.isFiring = util.tobool(isFiring)
   self.Entity:SetNetworkedBool("isFiring", self.isFiring)
end

function ENT:SetIsBeamCoherent(isBeamCoherent)
   self.isBeamCoherent = util.tobool(isBeamCoherent)
   self.Entity:SetNetworkedBool("isBeamCoherent", self.isBeamCoherent)
end

function ENT:CreateBeam()   
   inBeamInfo = EffectData()
	 inBeamInfo:SetEntity(self.Entity)
	 util.Effect("InBeam", inBeamInfo)
   
   local gateMarker = StargateExtras:GetGateMarker(self.localGate)
   
   local beam = ents.Create("env_laser")
   beam:SetPos(self:GetEmitterPos())
   beam:SetAngles(self.Entity:GetAngles())
   beam:SetOwner(self.Entity:GetOwner())
   beam:SetVar("Owner", self.Entity:GetVar("Owner", nil))
   beam:SetKeyValue("texture", "cable/crystal_beam1.vmt")
   beam:SetKeyValue("LaserTarget", gateMarker:GetName())
   beam:SetKeyValue("renderamt", "0")
   beam:SetKeyValue("rendercolor", self:GetBeamColour())
   beam:SetKeyValue("TextureScroll", "20")
   beam:SetKeyValue("width", "30")
   beam:SetKeyValue("damage", self.energyPerCycle)
   beam:SetKeyValue("dissolvetype", "2")
   
   beam:Spawn()
   beam:SetParent(self.Entity)
   beam:Fire("TurnOn", 1)
   
   if(self:IsBeamCoherent()) then
      timer.Simple(5, function() -- Spawn the beam after the effect finishes
        if(self.remoteGate == nil) then
          return
        end
        
      	local energyBeam = ents.Create("energy_beam")
      	
      	local Start = {}
      	Start.Entity = self.remoteGate.EventHorizon
      	Start.offset = StargateExtras:GetEntityCentre(Start.Entity) - Start.Entity:GetPos()
      	
      	local Damage = {}
      	Damage.amount = self.energyPerCycle / 2
      	Damage.radius = 50
      	
      	local beamSpeed = 2000
      	
      	energyBeam:Setup(self.Entity, 
      	                 Start,
      	                 Damage,
      	                 beamSpeed,
      	                 self:GetBeamColour(), 
      	                 "OutBeam")
      
     	  energyBeam:Spawn()
      	energyBeam:Activate()
      end)
      	
      self.Entity:SetNetworkedEntity("outBeam", energyBeam)
   end
   
   beam.subBeams = self:CreateSubBeams()
   
   beam.sound = CreateSound(beam, beamSoundPath)
   
   if(beam.sound) then
      beam.sound:Play()
   end
   
   return beam
end

function ENT:DestroyBeam(beam)
   self:DestroySubBeams(beam.subBeams)
   
   if(beam.sound) then
      beam.sound:Stop()
      beam.sound = nil
   end
   
   beam:Remove()
end

function ENT:CreateSubBeams()
   if(self.emitterMarker == nil || self.emitterMarker:IsValid() == false) then
      self.emitterMarker = ents.Create("info_target")
      self.emitterMarker:SetPos(self:GetEmitterPos())
      self.emitterMarker:SetName("EmitterMarker"..self.Entity:EntIndex())
      self.emitterMarker:Spawn()
      self.emitterMarker:SetParent(self.Entity)
   end
   
   local beams = {}
   local attachmentIDs =  
   {
      self.Entity:LookupAttachment("emitter1"),
      self.Entity:LookupAttachment("emitter2"),
      self.Entity:LookupAttachment("emitter3"),
      self.Entity:LookupAttachment("emitter4"),
      self.Entity:LookupAttachment("emitter5"),
      self.Entity:LookupAttachment("emitter6")
   }
   local numOfEmitters = table.getn(attachmentIDs)
   
   for attachmentNum = 1, numOfEmitters do
      local emitter = self.Entity:GetAttachment(attachmentIDs[attachmentNum])
      
      if(emitter == nil) then
         return beams
      end
      
      local beam = ents.Create("env_laser")
      beam:SetPos(emitter.Pos)
      beam:SetAngles(emitter.Ang)
      beam:SetOwner(self.Entity:GetOwner())
      beam:SetVar("Owner", self.Entity:GetVar("Owner", nil))
      beam:SetKeyValue("texture", "cable/crystal_beam1.vmt")
      beam:SetKeyValue("renderamt", "255")
      beam:SetKeyValue("rendercolor", self:GetBeamColour())
      beam:SetKeyValue("TextureScroll", "20")
      beam:SetKeyValue("width", "5")
      beam:SetKeyValue("damage", self.energyPerCycle / numOfEmitters)
      beam:SetKeyValue("dissolvetype", "2")
      beam:SetKeyValue("LaserTarget", self.emitterMarker:GetName())
      
      beam:Spawn()
      beam:SetParent(self.Entity)
      beam:Fire("TurnOn", 1)
      
      table.insert(beams, beam)
   end
   
   return beams
end

function ENT:DestroySubBeams(subBeams)
   for _, beam in pairs(subBeams) do
      beam:Remove()
   end
   
   if(self.emitterMarker && self.emitterMarker:IsValid()) then
      self.emitterMarker:Remove()
      self.emitterMarker = nil
   end
end

-- Stops the overloader firing
-- Returns: whether it was possible to stop firing
function ENT:StopFiring()
   if(self.isFiring == false) then
      return false
   end
   
   if(self.beam && self.beam:IsValid()) then 
      self:DestroyBeam(self.beam)
      self.beam = nil
   end
   
   -- Attempt to un-jam gates individually incase one of them has been destroyed
   
   if(self.remoteGate && self.remoteGate:IsValid()) then
      StargateExtras:UnJamGate(self.remoteGate)
      self.remoteGate:DeactivateStargate()
   end
   
   self.remoteGate = nil
   
   if(self.localGate && self.localGate:IsValid()) then
      StargateExtras:UnJamGate(self.localGate)
      self.localGate:DeactivateStargate()
   else
      self.localGate = nil
   end
   
   self:SetIsFiring(false)
   self.Entity:SetOverlayText("")
   
   return true
end

function ENT:HeatGate(gate)
   if(gate == nil) then
      error("The gate passed to HeatGate(gate) cannot be nil.\n")
      return false
   elseif(gate:IsValid() == false) then
      error("The gate passed to HeatGate(gate) was not a valid entity.\n")
      return false
   elseif(self.energyPerCycle <= 0) then
      return true
   elseif(gate.isOverloading == true) then
      return true
   end

   local addedEnergy = 0
   
   if(self.HasRD) then
      addedEnergy = self:ConsumeResource("energy", self.energyPerCycle)
   else
      addedEnergy = self.energyPerCycle
   end
   
   -- If the remote gate has its iris closed, it will absorb only half as much energy
   if(StargateExtras:IsIrisClosed(gate)) then
      addedEnergy = addedEnergy / 2
   end
   
   -- If the beam is coming out of the remote gate, only a quarter of its energy should build up in the gate
   if(self:IsBeamCoherent()) then
      addedEnergy = addedEnergy / 4
   end
   
   if(gate.excessPower == nil) then
      gate.excessPower = 0
   end
   
   addedEnergy = math.ceil(addedEnergy)
   gate.excessPower = gate.excessPower + addedEnergy
   
   if(self.HasRD) then
      -- Increase the energy capacity of the gate so that it can hold the additional energy build-up
      self:AddResource("energy", StargateExtras:GetStargateEnergyCapacity(gate) + addedEnergy)
      -- Supply the energy from the beam to the gate
      self:SupplyResource("energy", addedEnergy)
   end
   
   -- If the gate can no longer hold any more energy, make it explode
   if(gate.excessPower >= gate.excessPowerLimit) then
      gate.isOverloading = true
      
      local overloadEffect = EffectData()
      overloadEffect:SetEntity(self.Entity)
      util.Effect("Unstable", overloadEffect)
      
      timer.Simple(30, function() StargateExtras:DestroyStargate(gate) end, nil)
   end
   
   return true
end

-- Causes the gate overloader to take the given damage
function ENT:OnTakeDamage(damageInfo)
	if(damageInfo:GetInflictor():GetClass() == "env_laser" && ValidEntity(self.beam)) then
      for _, beam in pairs(self.beam.subBeams) do
         if(damageInfo:GetInflictor() == beam) then
            damageInfo:SetDamage(0)
            return
         end
      end
   end
   
   if(self.isDestructing == false && self.isCombatSystemEnabled ~= true) then
      self.Entity:TakePhysicsDamage(damageInfo)
      
      self.Entity:SetHealth(self.Entity:Health() - damageInfo:GetDamage())
       
      if(self.Entity:Health() <= 0) then
         self:Destruct()
      end
   end
end

-- Destroys the gate overloader
function ENT:Destruct()
   if(self.isDestructing) then
      return
   end
   
   self.isDestructing = true
   
   local blastRadius = 200
   local blastDamage = self.energyPerSecond / 50
   
   destructEffect = EffectData()
	destructEffect:SetOrigin(self.Entity:GetPos())
   destructEffect:SetRadius(blastRadius / 2)
	destructEffect:SetMagnitude(self.energyPerSecond / 10)
	util.Effect("Explosion", destructEffect, true, true)
   
   local owner = self.Entity:GetVar("Owner", self.Entity)
   util.BlastDamage(self.Entity, owner, self.Entity:GetPos(), blastRadius, blastDamage)
    
	self.Entity:Remove()
end

-- Does cleanup if the gate overloader is being removed
function ENT:OnRemove()
	self:Disarm()
   
  StargateExtras:UnregisterEntity(self)
  StarGate.WireRD.OnRemove(self)
end
